#!/bin/bash
# This script should be called from udev or systemd.
source /lib/dracut-lib.sh
set -e

# this label is currently hardcoded in cosa:
# https://github.com/coreos/coreos-assembler/blob/60bb29672483cdb1cbabf08994bfd714dddaba56/src/create_disk.sh#L151
FS_LABEL=crypt_rootfs
CLI_CMD=${1}; shift;
RUN_D="/run/coreos"
CLEVIS_JSON="${RUN_D}/clevis.json"
NET=0

# For the null-cipher case, we skip dm-crypt and use dm-linear. The
# header offset is used to map the start of the device mapper target.
# This is currently hardcoded; it's unlikely that cryptsetup will change,
# at least hopefully not before we replace this with Ignition.
LUKS_HEADER_OFFSET_SECTORS=32768

# we do realpath here since we temporarily lose the label when reencrypting
dev=$(realpath /dev/disk/by-label/${FS_LABEL})

# This is needed otherwise cryptsetup will fail
mkdir -p /run/cryptsetup
mkdir -p "${RUN_D}"

msg() {
    echo "coreos-cryptfs: ${@}"
}

fatal() {
    msg ${@};
    exit 1;
}

load_kmods() {
    # Load kernel modules only when required. In BZ# 1803130,
    # it was discovered that virtual hardware that lacks an entrophy
    # source fails to boot. Ensure that we only load the required modules
    # if needed.
    modprobe -a dm_crypt loop
}

cmd_waiter() {
    local c=0;
    until ${@} >> /dev/null;
    do
        sleep 0.5;
        c=$(($c + 1))
        if [ "${c}" -gt 60 ]; then
            return 1
        fi
    done
    return
}

net_waiter() {
    if [ "${NET}" -eq 1 ]; then
        return 0
    fi

    msg "Checking for default route."
    cmd_waiter 'ip route show default' ||
        fatal "unable find default network"
    msg "Waiting for DNS resolver to appear."
    cmd_waiter 'test -e /etc/resolv.conf' ||
        fatal "failed to find /etc/resolv.conf"
    NET=1
}

coreos_token() {
    local dev="${1:?dev must be the first argument}"

    # Check if the token is in the LUKS header.
    local coreos_id="$(cryptsetup luksDump ${dev} | sed -rn 's|^\s+([0-9]+): coreos|\1|p')"
    if [ -z "${coreos_id}" ]; then
        msg "${dev} does not have a coreos LUKS header."
        exit 0
    fi

    # Read the token and parse it.
    local token="$(cryptsetup token export --token-id ${coreos_id} ${dev})"
    local token_type="$(jq -erM '.type //""' <<<${token})"
    if [ "${token_type}" != "coreos" ]; then
        cat << EOM
COREOS: DEVICE ${dev} MISSING LUKS TOKEN ${token_type}"
        Tokens must be of type 'coreos' and have an 'key'
        that can unlocked the device.
EOM
        exit 1
    fi
    echo "${token}"
}

coreos_key() {
    local dev="${1:?dev must be the first argument}"
    jq -erM '.key' <<<"$(coreos_token ${dev})"
}

try_open() {
    # Determine if the volume is clevis managed or automatic unlock.
    # The automatic clevis hooks will NOT work since they rely on the systemd
    # crypttab generator.
    local clevis_id="$(cryptsetup luksDump ${dev} | sed -rn 's|^\s+([0-9]+): clevis|\1|p')"
    if [ -n "${clevis_id}" ]; then
        load_kmods
        local pin=$(cryptsetup token export --token-id "${cleivs_id}" "${dev}" \
            | jq -rM '.jwe.protected' | base64 -d | jq -rM '.clevis.pin')
        msg "${dev} is configured for Clevis pin '${pin}'"
        [ "${pin}" == "tpm2" ] || net_waiter
        clevis-luks-unlock -d "${dev}"
        exit $?;
    fi

    local cipher=$(cryptsetup luksDump ${dev} | awk '/cipher/{print$NF;exit}')
    msg "${dev} uses the '${cipher}'"
    if [ "${cipher}" == "cipher_null-ecb" ]; then
        # for null-cipher, us a direct dm-linear target
        # this avoids the cryptsetup performance penalty.
        msg "${dev} is not encrypted. Device will be mounted as device-mapper linear target."

        # We inline a growpart here since it's simpler than trying to reconfigure dm-linear
        local major=$((0x$(stat -c '%t' "${dev}")))
        local minor=$((0x$(stat -c '%T' "${dev}")))
        local devpath=$(realpath "/sys/dev/block/${major}:${minor}")
        local partition=$(cat "$devpath/partition")
        local parent_path=$(dirname "$devpath")
        local parent_device=/dev/$(basename "${parent_path}")

        # TODO: make this idempotent, and don't error out if
        # we can't resize.
        growpart "${parent_device}" "${partition}" || true

        local dev_size=$(($(blockdev --getsize ${dev}) - ${LUKS_HEADER_OFFSET_SECTORS}))
        echo "0 ${dev_size} linear ${dev} ${LUKS_HEADER_OFFSET_SECTORS}" \
            | dmsetup create coreos-luks-root-nocrypt
    else
        local key="$(coreos_key ${dev})"
        (echo -en "${key:-x}" | cryptsetup luksOpen "${dev}" coreos-luks-root) ||
           fatal "FAILED to open ${dev}"
    fi
}

case "${CLI_CMD}" in
    open)    try_open; exit;;
          *) msg "option ${CLI_CMD} is not supported";;
esac
